﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.TextTemplating.VSHost;
using System.CodeDom;
using System.IO;
using Microsoft.Win32;
using System.Windows.Forms;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.TextTemplating;
using EnvDTE;
using System.Reflection;
using Perovich.GameObjects.Tools.Util;
using System.Xml;
using Perovich.GameObjects.Attributes;
using System.CodeDom.Compiler;
using System.Xml.Serialization;
namespace Perovich.GameObjects.Tools
{
    [Guid("A4F30983-CAD7-454C-BB27-00BCEECF2A67")]
    [CustomTool("EntityCodeGenerator", "Create an Entity object from Xml design")]
    public class EntityCodeGenerator : BaseCodeGeneratorWithSite
    {

        //private EntityGeneratorTemplate gen;

        public EntityCodeGenerator()
        {
            
        }

        string GetTypeOfConstructorParameter(string tstring, string paramName)
        {
            Type t = type(tstring);
            var ctors = t.GetConstructors();
            foreach (var ctor in ctors)
            {
                foreach (var param in ctor.GetParameters())
                {
                    if (param.Name.Equals(paramName))
                    {
                        return param.ParameterType.ToString();
                    }
                }
            }
            return null;
        }

        Type GetTypeOfProperty(string name, Dictionary<string, Type> autoprops, EntityDefine entity)
        {
            if (autoprops.ContainsKey(name))
                return autoprops[name];
            else
                return GetTypeOfProperty(entity.Property.First(o => { return o.name.Equals(name); }));
        }

        Type GetTypeOfProperty(string type, string propName)
        {
            Type t = Type.GetType(type, false, true);
            var a = typeof(Perovich.GameObjects.Components.Position2D);
            string assQualifier = a.AssemblyQualifiedName;
            if (t == null)
            {
                t = Type.GetType(assQualifier.Replace("Position2D", type), false, true);
            }
            if (t == null)
            {
                t = Type.GetType(FileNamespace + type, false, true);
            }
            var props = t.GetProperties();
            foreach (var prop in props)
            {
                if (prop.Name.Equals(propName))
                {
                    return prop.PropertyType;
                }
            }
            return null;
        }

        Type GetTypeOfProperty(Perovich.GameObjects.Tools.Util.Property p)
        {
            return GetTypeOfProperty(p.target.Split('.')[0], p.target.Split('.')[1]);
        }

        AutomaticFunctionAttribute[] GetAutomaticFunctions(string tstring)
        {
            AutomaticFunctionAttribute[] attrs = (AutomaticFunctionAttribute[])type(tstring).GetCustomAttributes(typeof(AutomaticFunctionAttribute), false);
            return attrs;
        }

        AutomaticEventAttribute[] GetAutomaticEvents(string tstring)
        {
            AutomaticEventAttribute[] attrs = (AutomaticEventAttribute[])type(tstring).GetCustomAttributes(typeof(AutomaticEventAttribute), false);
            return attrs;
        }

        AutomaticInterfaceAttribute[] GetAutomaticInterfaces(string tstring)
        {
            AutomaticInterfaceAttribute[] attrs = (AutomaticInterfaceAttribute[])type(tstring).GetCustomAttributes(typeof(AutomaticInterfaceAttribute), false);
            return attrs;
        }

        Type type(string tstring)
        {
            Type t = Type.GetType(tstring, false, true);
            var a = new Perovich.GameObjects.Components.Position2D();
            string assQualifier = a.GetType().AssemblyQualifiedName;
            if (t == null)
            {
                t = Type.GetType(assQualifier.Replace("Position2D", tstring), false, true);
            }
            return t;
        }

        AutomaticPropertyAttribute[] GetAutomaticProperties(string tstring)
        {
            return GetAutomaticProperties(type(tstring));
        }

        AutomaticPropertyAttribute[] GetAutomaticProperties(Type t)
        {
            return (AutomaticPropertyAttribute[])t.GetCustomAttributes(typeof(AutomaticPropertyAttribute), false);
        }

        AutomaticImportAttribute[] GetAutomaticImports(string tstring)
        {
            return GetAutomaticImports(type(tstring));
        }

        AutomaticImportAttribute[] GetAutomaticImports(Type t)
        {
            return (AutomaticImportAttribute[])t.GetCustomAttributes(typeof(AutomaticImportAttribute), false);
        }


        MemberAttributes getAttri(string acc)
        {
            if (acc == null)
            {
                return MemberAttributes.Public | MemberAttributes.Final;
            }
            else if (acc == "")
            {
                return 0;
            }
            else
            {
                string l = acc.ToLower();
                MemberAttributes ret = MemberAttributes.Final | MemberAttributes.Private;
                if (l.Contains("public"))
                {
                    ret ^= MemberAttributes.Private;
                    ret |= MemberAttributes.Public;
                }
                else if (l.Contains("protected"))
                {
                    ret ^= MemberAttributes.Private;
                    if (!l.Contains("internal"))
                    {
                        ret |= MemberAttributes.Family;
                    }
                    else
                    {
                        ret |= MemberAttributes.FamilyOrAssembly;
                    }
                }
                if (l.Contains("internal"))
                {
                    if (!l.Contains("protected"))
                    {
                        ret ^= MemberAttributes.Private;
                        ret |= MemberAttributes.Assembly;
                    }
                }
                if (l.Contains("virtual"))
                {
                    ret ^= MemberAttributes.Final;
                }
                if (l.Contains("new"))
                {
                    ret |= MemberAttributes.New;
                }

                return ret;
            }
        }

        object getLitteralValue(string str)
        {
            double d;
            float f;
            short s;
            int i;
            long l;
            if (str.Contains("."))
            {
                if (double.TryParse(str, out d))
                {
                    return d;
                }
                if (float.TryParse(str.Split('f')[0], out f))
                {
                    return f;
                }
            }
            if (short.TryParse(str, out s))
            {
                return s;
            }
            if (int.TryParse(str, out i))
            {
                return i;
            }
            if (long.TryParse(str, out l))
            {
                return l;
            }
            return str;
        }




        protected override byte[] GenerateCode(
            string inputFileName, string inputFileContent)
        {
            try
            {
                Perovich.GameObjects.Tools.Util.ProjectExt MyProject = null;
                Solution solution = Dte.Solution;
                var projectItm = solution.FindProjectItem(inputFileName);
                if (projectItm != null)
                    MyProject = new Perovich.GameObjects.Tools.Util.ProjectExt(projectItm.ContainingProject);
                var doc = new XmlDocument();
                doc.LoadXml(inputFileContent);
                var entity = new EntityDefine(doc);

                List<AutoProperty> properties = new List<AutoProperty>();
                List<AutoComponentConstructor> components = new List<AutoComponentConstructor>();
                List<AutoFunction> functions = new List<AutoFunction>();
                List<AutoEvent> events = new List<AutoEvent>();
                List<AutoInterface> interfaces = new List<AutoInterface>();
                List<AutoPropertyInitializer> intiializers = new List<AutoPropertyInitializer>();

                List<CodeMemberField> fields = new List<CodeMemberField>();
                List<CodeAssignStatement> fieldAssigns = new List<CodeAssignStatement>();

                List<string> imports = new List<string>();
                List<string> assemblies = new List<string>();


                foreach (var p in entity.Property)
                {
                    if (!string.IsNullOrEmpty(p.target))
                    {
                        var findexpr = new CodePropertyReferenceExpression(new CodeMethodInvokeExpression(new CodeMethodReferenceExpression(new CodeThisReferenceExpression(),"Find",new CodeTypeReference(p.target.Split('.')[0]))),p.target.Split('.')[1]);
                        var auto = new AutoProperty(p.name, GetTypeOfProperty(p.target.Split('.')[0], p.target.Split('.')[1]).ToString(), findexpr, getAttri(p.access));
                        auto.Summary = p.summary;
                        auto.Value = p.returns;
                        properties.Add(auto);
                    }
                    else
                    {
                        var auto = new AutoProperty(p.name, p.type, attri: getAttri(p.access));
                        auto.Summary = p.summary;
                        auto.Value = p.returns;
                        properties.Add(auto);
                    }
                }

                foreach (var c in entity.Component)
                {
                    var fei = new CodeMemberField(type(c.type), "_" + c.type);
                    fei.Attributes = MemberAttributes.Private;
                    fields.Add(fei);
                    var expr = new CodeMethodInvokeExpression(new CodeMethodReferenceExpression(new CodeThisReferenceExpression(),"Find",new CodeTypeReference(type(c.type))));
                    fieldAssigns.Add(new CodeAssignStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(),"_" + c.type),expr));
                    var meExpr = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_" + c.type);
                    foreach (var p in GetAutomaticProperties(c.type))
                    {
                        if (p.CType != null)
                        {
                            properties.Add((AutoProperty)p.CType.GetMethod(p.Funct, new Type[] { typeof(CodeExpression) }).Invoke(null, new object[] { meExpr }));
                        }
                        else if (p.Type == null)
                        {
                            var findexpr = new CodePropertyReferenceExpression(meExpr, p.Target.Split('.')[1]);
                            var auto = new AutoProperty(p.Name, GetTypeOfProperty(p.Target.Split('.')[0], p.Target.Split('.')[1]).Name, findexpr, getAttri(p.Access));
                            auto.Summary = p.XmlSummary;
                            auto.Value = p.XmlReturns;
                            auto.IsReadOnly = p.IsReadOnly;
                            properties.Add(auto);
                        }
                        else
                        {
                            var auto = new AutoProperty(p.Name, p.Type.Name, attri: getAttri(p.Access));
                            auto.Summary = p.XmlSummary;
                            auto.Value = p.XmlReturns;
                            auto.IsReadOnly = p.IsReadOnly;
                            properties.Add(auto);
                        }
                    }

                    foreach (var i in GetAutomaticImports(c.type))
                    {
                        if (i.ARef)
                        {
                            if (!assemblies.Any((s => string.Compare(s, i.NS) == 0)))
                            {
                                assemblies.Add(i.NS);
                            }
                        }
                        else
                        {
                            if (!imports.Any((s => string.Compare(s, i.NS) == 0)))
                            {
                                imports.Add(i.NS);
                            }
                        }
                    }


                    components.Add(new AutoComponentConstructor(type(c.type), new CodeExpression[] {}));

                    foreach (var func in GetAutomaticFunctions(c.type))
                    {
                        functions.Add((AutoFunction)func.Type.GetMethod(func.Method, new Type[] { typeof(CodeExpression) }).Invoke(null, new object[] { meExpr }));
                    }

                    foreach (AutomaticEventAttribute ev in GetAutomaticEvents(c.type))
                    {
                        events.Add((AutoEvent)ev.Type.GetMethod(ev.Method, new Type[] { typeof(CodeExpression) }).Invoke(null, new object[] { meExpr }));
                    }

                    foreach (AutomaticInterfaceAttribute Iface in GetAutomaticInterfaces(c.type))
                    {
                        interfaces.Add(new AutoInterface(Iface.Type.Name));
                    }

                }

                foreach (var i in entity.Init)
                {
                    var prop = properties.Find(p =>
                    {
                        return p.Name == i.name;
                    });
                    intiializers.Add(new AutoPropertyInitializer(i.name, prop.Type, new CodePrimitiveExpression(getLitteralValue(i.value))));
                }


                foreach (var f in entity.Function)
                {
                    List<CodeParameterDeclarationExpression> param = new List<CodeParameterDeclarationExpression>();
                    List<string> paramDescs = new List<string>();
                    foreach (var p in f.Param)
                    {
                        param.Add(new CodeParameterDeclarationExpression(p.type, p.name));
                        paramDescs.Add(p.desc);
                    }
                    var fun = new AutoFunction(f.name, f.type, param.ToArray(), getAttri(f.access));
                    fun.Summary = f.summary;
                    fun.Returns = f.returns;
                    fun.Param = paramDescs.ToArray();
                    foreach (string line in f.Text.Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries))
                    {
                        fun.AddLine(new CodeSnippetStatement(line));
                    }
                    functions.Add(fun);
                }

                foreach (var i in entity.Interface)
                {
                    interfaces.Add(new AutoInterface(i.type));
                }

                foreach (var e in entity.Event)
                {
                    var eve = new AutoEvent(e.name, e.type, attri: getAttri(e.access));
                    eve.Summary = e.summary;
                    events.Add(eve);
                }

                MyProject.AddReference("Microsoft.Xna.Framework.Game", "Microsoft.Xna.Framework.Game");
                MyProject.AddReference("Perovich.GameObjects", "Perovich.GameObjects");
                MyProject.AddReference("Perovich.GameObjects.Components", "Perovich.GameObjects.Components");
                MyProject.AddReference("Perovich.GameObjects.Entities", "Perovich.GameObjects.Entities");

                foreach (var r in assemblies)
                {
                    MyProject.AddReference(r, r);
                }

                var targetUnit = new CodeCompileUnit();
                
                var ns  = new System.CodeDom.CodeNamespace(FileNamespace);
                ns.Imports.Add(new CodeNamespaceImport("System"));
                ns.Imports.Add(new CodeNamespaceImport("Perovich.GameObjects.Interfaces"));
                ns.Imports.Add(new CodeNamespaceImport("Perovich.GameObjects.Components"));
                ns.Imports.Add(new CodeNamespaceImport("Perovich.GameObjects.Entities"));
                foreach (var i in imports)
                {
                    ns.Imports.Add(new CodeNamespaceImport(i));
                }
                targetUnit.Namespaces.Add(ns);

                var genClass = new CodeTypeDeclaration(entity.name);
                genClass.IsClass = true;
                genClass.IsPartial = true;
                genClass.TypeAttributes = TypeAttributes.Public;
                genClass.BaseTypes.Add(new CodeTypeReference("Entity"));
                foreach (var i in interfaces)
                {
                    genClass.BaseTypes.Add(new CodeTypeReference(i.Type));
                }
                ns.Types.Add(genClass);

                if (!string.IsNullOrEmpty(entity.summary)) genClass.Comments.Add(new CodeCommentStatement("<summary>" + entity.summary + "<summary/>", true));

                foreach (var f in fields)
                {
                    genClass.Members.Add(f);
                }

                foreach (var p in properties)
                {
                    genClass.Members.Add(p.Prop);
                    if (p.HasField)
                    {
                        genClass.Members.Add(p.Fie);
                    }
                }

                var cons = new CodeConstructor();
                var para1 = new CodeParameterDeclarationExpression(typeof(string),"name");
                var para2 = new CodeParameterDeclarationExpression("ICanHoldComponents", "parent");
                cons.Parameters.Add(para1);
                cons.Parameters.Add(para2);
                cons.Attributes = MemberAttributes.Public;
                cons.BaseConstructorArgs.Add(new CodeArgumentReferenceExpression("name"));
                cons.BaseConstructorArgs.Add(new CodeArgumentReferenceExpression("parent"));

                cons.Comments.Add(new CodeCommentStatement("<summary>Constructs a new instance of the <see cref=\"" + genClass.Name + "\">" + genClass.Name + "</see> class.</summary>", true));
                cons.Comments.Add(new CodeCommentStatement("<param name=\"name\">The name of the new <see cref=\"" + genClass.Name + "\">" + genClass.Name + "</see>.</param>", true));
                cons.Comments.Add(new CodeCommentStatement("<param name=\"parent\">The parent of the new <see cref=\"" + genClass.Name + "\">" + genClass.Name + "</see>.</param>", true));
                foreach (var c in components)
                {
                    cons.BaseConstructorArgs.Add(c.Ctor);
                }
                foreach (var a in fieldAssigns) {
                    cons.Statements.Add(a);
                }
                foreach (var i in intiializers)
                {
                    cons.Statements.Add(i.Init);
                }
                //cons.Statements.Add(new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), "Construct"));
                genClass.Members.Add(cons);

                foreach (var f in functions)
                {
                    genClass.Members.Add(f.Meth);
                }


                foreach (var e in events)
                {
                    genClass.Members.Add(e.Ev);
                    if (!e.IsAutoEvent)
                    {
                        genClass.Members.Add(e.Meth);
                        cons.Statements.Add(e.Atch);
                    }
                }

                CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");
                CodeGeneratorOptions options = new CodeGeneratorOptions();
                options.BracingStyle = "C";
                using (MemoryStream s = new MemoryStream()) {
                    using (StreamWriter wr = new StreamWriter(s)) {
                        provider.GenerateCodeFromCompileUnit(targetUnit, wr, options);
                    }
                    return s.ToArray();
                }

                
            }
            catch (Exception e)
            {
                return Encoding.ASCII.GetBytes("/*" + e.ToString() + "*/");
            }
        }


        // This method is being called every time the attached xml is saved.
        protected byte[] _GenerateCode(
            string inputFileName, string inputFileContent)
        {
            try
            {
                ITextTemplating t4 = GlobalServiceProvider.GetService(typeof(STextTemplating)) as ITextTemplating;
                ITextTemplatingSessionHost sessionHost = t4 as ITextTemplatingSessionHost;
                sessionHost.Session = sessionHost.CreateSession();
                sessionHost.Session["XmlInput"] = inputFileName;
                sessionHost.Session["DefaultNamespace"] = FileNamespace;

                Solution solution = Dte.Solution;
                foreach (Project project in solution.Projects)
                {
                    foreach (ProjectItem item in project.ProjectItems)
                    {
                        if (inputFileName.Contains(item.Name))
                        {
                            sessionHost.Session["project"] = new Perovich.GameObjects.Tools.Util.ProjectExt(project);
                        }
                    }
                }



                string resultFile = t4.ProcessTemplate(inputFileName, Resources.EntityGeneratorTemplate);

                // Try to generate the wrapper file.
                return Encoding.ASCII.GetBytes(resultFile);
            }
            catch (Exception ex)
            {
                // In case of a failure - print the exception 
                // as a comment on the .cs file.
                return Encoding.ASCII.GetBytes("/*" + ex.ToString() + "*/");
                throw;
            }
        }

        public override string GetDefaultExtension()
        {
            return ".cs";
        }


        

        private static Guid CSharpCategoryGuid =
            new Guid("{FAE04EC1-301F-11D3-BF4B-00C04F79EFBC}");
        private const string VisualStudioVersion = "10.0";


        [ComRegisterFunction]
        public static void RegisterClass(Type t)
        {
            GuidAttribute guidAttribute = getGuidAttribute(t);
            CustomToolAttribute customToolAttribute = getCustomToolAttribute(t);
            using (RegistryKey key = Registry.LocalMachine.CreateSubKey(
              GetKeyName(CSharpCategoryGuid, customToolAttribute.Name)))
            {
                key.SetValue("", customToolAttribute.Description);
                key.SetValue("CLSID", "{" + guidAttribute.Value + "}");
                key.SetValue("GeneratesDesignTimeSource", 1);
            }
        }

        [ComUnregisterFunction]
        public static void UnregisterClass(Type t)
        {
            CustomToolAttribute customToolAttribute = getCustomToolAttribute(t);
            Registry.LocalMachine.DeleteSubKey(GetKeyName(
              CSharpCategoryGuid, customToolAttribute.Name), false);
        }

        internal static GuidAttribute getGuidAttribute(Type t)
        {
            return (GuidAttribute)getAttribute(t, typeof(GuidAttribute));
        }

        internal static CustomToolAttribute getCustomToolAttribute(Type t)
        {
            return (CustomToolAttribute)getAttribute(t, typeof(CustomToolAttribute));
        }

        internal static Attribute getAttribute(Type t, Type attributeType)
        {
            object[] attributes = t.GetCustomAttributes(attributeType, /* inherit */ true);
            if (attributes.Length == 0)
                throw new Exception(
                  String.Format("Class '{0}' does not provide a '{1}' attribute.",
                  t.FullName, attributeType.FullName));
            return (Attribute)attributes[0];
        }

        internal static string GetKeyName(Guid categoryGuid, string toolName)
        {
            return
              String.Format("SOFTWARE\\Microsoft\\VisualStudio\\" + VisualStudioVersion +
                "\\Generators\\{{{0}}}\\{1}\\", categoryGuid, toolName);
        }


    }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    public class CustomToolAttribute : Attribute
    {
        /// <summary></summary>
        protected string _description;

        /// <summary></summary>
        protected string _name;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        public CustomToolAttribute(string name) :
            this(name, "")
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        /// <param name="description"></param>
        public CustomToolAttribute(string name, string description)
        {
            _name = name;
            _description = description;
        }

        /// <summary>
        /// 
        /// </summary>
        public string Name
        {
            get { return _name; }
        }

        /// <summary>
        /// 
        /// </summary>
        public string Description
        {
            get { return _description; }
        }
    }
}

